// Copyright (c) 2007, Brian Duff

// See LICENSE for full license details.

package org.dubh.jdant;

import java.io.File;
import java.io.IOException;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import java.util.logging.Logger;

import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;


/**
 * A version of the <tt>HashStructureIO</tt> API in the Oracle IDE. This
 * class can parse an XML file in hash structure format and instantiate a
 * <tt>ROHashStructure</tt> instance.
 *
 * @author brian.duff@dubh.org
 */
public final class ROHashStructureIO
{
  private final Logger LOG = Logger.getLogger( "org.dubh.jdant" );
  
  /**
   * Loads an XML file into a {@link ROHashStructure}.
   * 
   * @param file the file to load.
   * @return the <tt>ROHashStructure</tt>.
   * @throws IOException if an error occurs parsing the XML file.
   * 
   * @throws NullPointerException if <tt>file</tt> is <tt>null</tt>.
   */
  public ROHashStructure load( File file ) throws IOException
  {
    LOG.fine( "Loading " + file );
    if ( file == null ) throw new NullPointerException( "file is null" );
    
    HashStructureHandler handler = new HashStructureHandler( file );

    try
    {
      Util.newSAXParser().parse( file, handler );
      return new ROHashStructure( handler.getRootMap() );
    }
    catch (SAXException e)
    {
      String message = file.getName();
      
      if ( e instanceof SAXParseException )
      {
        SAXParseException spe = (SAXParseException) e;
        message += ":" + spe.getLineNumber() + "," + spe.getColumnNumber();
      }
      message += ": " + e.getMessage();
      
      IOException ioe = new IOException( message );
      ioe.initCause( e );
      throw ioe;
    }
  }
  
  private static class HashStructureHandler extends DefaultHandler
  {
    private final Stack<Object> _stack = new Stack<Object>(); // in 1.6, replace this with a Deque.
    private final File _file;
    private Map<String,Object> _rootMap = null;
    private Locator _locator;

    HashStructureHandler( File file )
    {
      _file = file;
    }
    
    Map<String,Object> getRootMap()
    {
      return _rootMap;
    }

    @Override
    public void startElement(String uri, String localName, String qName,
                             Attributes attributes)
      throws SAXException
    {
      String name = attributes.getValue( "n" );

      if ( _rootMap == null )
      {
        // At the root element. 
        _rootMap = new HashMap<String, Object>();
        _stack.push( _rootMap );
      }
      else if ( "hash".equals( localName ) )
      {
        Map<String,Object> value = new HashMap<String, Object>();
        addToCurrentLevel( name, new ROHashStructure( value ) );
        _stack.push( value );
      }
      else if ( "list".equals( localName ) )
      {
        List<Object> value = new ArrayList<Object>();
        addToCurrentLevel( name, value );
        _stack.push( value );
      }
      else if ( "value".equals( localName ) )
      {
        addToCurrentLevel( name, attributes.getValue( "v" ) );
      }
      else if ( "string".equals( localName ) )
      {
        addToCurrentLevel( name, attributes.getValue( "v" ) );
      }
      else if ( "url".equals( localName ) )
      {
        String path = attributes.getValue( "path" );
        // Not a bug if path is null, we just skip it.
        if ( path != null ) addToCurrentLevel( name, toURL( path ) );
      }
      
    }
    
    private URL toURL( String path ) throws SAXException
    {
      File absFile = new File( _file.getParentFile(), path ).getAbsoluteFile();

      try
      {
        return new URL( "file", "", absFile.getPath() );
      }
      catch ( MalformedURLException e )
      {
        throw new SAXParseException( "cannot resolve url " + absFile, _locator );
      }
    }
    
    private void addToCurrentLevel( String name, Object value )
      throws SAXException
    {
      Object currentCollection = _stack.peek();
      if ( currentCollection instanceof List )
      {
        ((List<Object>)currentCollection).add( value );
      }
      else if ( currentCollection instanceof Map )
      {
        if ( name == null ) throw new SAXParseException( "name expected", _locator );
        ((Map)currentCollection).put( name, value );
      }
    }

    @Override
    public void endElement(String uri, String localName, String qName )
      throws SAXException
    {
      if ( "hash".equals( localName ) || "list".equals( localName ) )
      {
        if ( _stack.isEmpty() ) return;
        _stack.pop();
      }
    }

    @Override
    public void setDocumentLocator( Locator locator )
    {
      super.setDocumentLocator( locator );
      _locator = locator;
    }
  }
}
